{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Frequently asked questions\n",
    "\n",
    "* [How do I load/save state machine configurations with json/yaml](#How-do-I-load/save-state-machine-configurations-with-json/yaml)\n",
    "* [How to use transitions with django models?](#How-to-use-transitions-with-django-models?)\n",
    "* [transitions memory footprint is too large for my Django app and adding models takes too long.](#transitions-memory-footprint-is-too-large-for-my-Django-app-and-adding-models-takes-too-long.) \n",
    "* [Is there a during callback which is called when no transition has been successful?](#Is-there-a-'during'-callback-which-is-called-when-no-transition-has-been-successful?)\n",
    "* [How to have a dynamic transition destination based on a function's return value?](#How-to-have-a-dynamic-transition-destination-based-on-a-function's-return-value)\n",
    "* [Machine.get_triggers should only show valid transitions based on some conditions.](#Machine.get_triggers-should-only-show-valid-transitions-based-on-some-conditions.)\n",
    "* [Transitions does not add convencience methods to my model](#Transitions-does-not-add-convencience-methods-to-my-model)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### How do I load/save state machine configurations with json/yaml\n",
    "\n",
    "The easiest way to load a configuration is by making sure it is structured just as the `Machine` constructor. Your first level elements should be `name`, `transitions`, `states` and so on. When your yaml/json configuration is loaded, you can add your model programatically and pass the whole object to `Machine`.\n",
    "\n",
    "#### Loading a JSON configuration"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from transitions import Machine\n",
    "import json\n",
    "\n",
    "\n",
    "class Model:\n",
    "\n",
    "    def say_hello(self, name):\n",
    "        print(f\"Hello {name}!\")\n",
    "\n",
    "\n",
    "# import json\n",
    "json_config = \"\"\"\n",
    "{\n",
    "  \"name\": \"MyMachine\",\n",
    "  \"states\": [\n",
    "    \"A\",\n",
    "    \"B\",\n",
    "    { \"name\": \"C\", \"on_enter\": \"say_hello\" }\n",
    "  ],\n",
    "  \"transitions\": [\n",
    "    [\"go\", \"A\", \"B\"],\n",
    "    {\"trigger\": \"hello\", \"source\": \"*\", \"dest\": \"C\"}\n",
    "  ],\n",
    "  \"initial\": \"A\"\n",
    "}\n",
    "\"\"\"\n",
    "\n",
    "model = Model()\n",
    "\n",
    "config = json.loads(json_config)\n",
    "config['model'] = model  # adding a model to the configuration\n",
    "m = Machine(**config)  # **config unpacks arguments as kwargs\n",
    "assert model.is_A()\n",
    "model.go()\n",
    "assert model.is_B()\n",
    "model.hello(\"world\")  # >>> Hello world!\n",
    "assert model.state == 'C'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####  Loading a YAML configuration\n",
    "\n",
    "This example uses [pyyaml](https://pypi.org/project/PyYAML/)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from transitions import Machine\n",
    "import yaml\n",
    "\n",
    "\n",
    "class Model:\n",
    "\n",
    "    def say_hello(self, name):\n",
    "        print(f\"Hello {name}!\")\n",
    "\n",
    "        \n",
    "yaml_config = \"\"\"\n",
    "---\n",
    "\n",
    "name: \"MyMachine\"\n",
    "\n",
    "states:\n",
    "  - \"A\"\n",
    "  - \"B\"\n",
    "  - name: \"C\"\n",
    "    on_enter: \"say_hello\"\n",
    "\n",
    "transitions:\n",
    "  - [\"go\", \"A\", \"B\"]\n",
    "  - {trigger: \"hello\", source: \"*\", dest: \"C\"}\n",
    "\n",
    "initial: \"A\"\n",
    "\"\"\"\n",
    "\n",
    "model = Model()\n",
    "\n",
    "config = yaml.safe_load(yaml_config)  \n",
    "config['model'] = model  # adding a model to the configuration\n",
    "m = Machine(**config)  # **config unpacks arguments as kwargs\n",
    "assert model.is_A()\n",
    "model.go()\n",
    "assert model.is_B()\n",
    "model.hello(\"world\")  # >>> Hello world!\n",
    "assert model.state == 'C'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Exporting YAML or JSON\n",
    "\n",
    "A default `Machine` does not keep track of its configuration but `transitions.extensions.markup.MarkupMachine` does. \n",
    "`MarkupMachine` cannot just be used to export your configuration but also to visualize or instrospect your configuration conveniently.\n",
    "Is is also the foundation for `GraphMachine`. You will see that `MarkupMachine` will always export every attribute even unset values. This makes such exports visually cluttered but easier to automatically process.\n",
    "If you plan to use such a configuration with a 'normal' `Machine`, you should remove the `models` attribute from the markup since `Machine` cannot process it properly.\n",
    "If you pass the (stored and loaded) configuration to another `MarkupMachine` however, it will attempt to create and initialize models for you."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "#export\n",
    "from transitions.extensions.markup import MarkupMachine\n",
    "import json\n",
    "import yaml\n",
    "\n",
    "\n",
    "class Model:\n",
    "\n",
    "    def say_hello(self, name):\n",
    "        print(f\"Hello {name}!\")\n",
    "\n",
    "\n",
    "model = Model()\n",
    "m = MarkupMachine(model=None, name=\"ExportedMachine\")\n",
    "m.add_state('A')\n",
    "m.add_state('B')\n",
    "m.add_state('C', on_enter='say_hello')\n",
    "m.add_transition('go', 'A', 'B')\n",
    "m.add_transition(trigger='hello', source='*', dest='C')\n",
    "m.initial = 'A'\n",
    "m.add_model(model)\n",
    "model.go()\n",
    "\n",
    "print(\"JSON:\")\n",
    "print(json.dumps(m.markup, indent=2))\n",
    "print('\\nYAML:')\n",
    "print(yaml.dump(m.markup))\n",
    "\n",
    "config2 = json.loads(json.dumps(m.markup))  # simulate saving and loading\n",
    "m2 = MarkupMachine(markup=config2)\n",
    "model2 = m2.models[0]  # get the initialized model\n",
    "assert model2.is_B()  # the model state was preserved\n",
    "model2.hello('again')  # >>> Hello again!\n",
    "assert model2.state == 'C'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### How to use `transitions` with django models?\n",
    "\n",
    "In [this comment](https://github.com/pytransitions/transitions/issues/146#issuecomment-300277397) **proofit404** provided a nice example about how to use `transitions` and django together:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from django.db import models\n",
    "from django.db.models.signals import post_init\n",
    "from django.dispatch import receiver\n",
    "from django.utils.translation import ugettext_lazy as _\n",
    "from transitions import Machine\n",
    "\n",
    "\n",
    "class ModelWithState(models.Model):\n",
    "    ASLEEP = 'asleep'\n",
    "    HANGING_OUT = 'hanging out'\n",
    "    HUNGRY = 'hungry'\n",
    "    SWEATY = 'sweaty'\n",
    "    SAVING_THE_WORLD = 'saving the world'\n",
    "    STATE_TYPES = [\n",
    "        (ASLEEP, _('asleep')),\n",
    "        (HANGING_OUT, _('hanging out')),\n",
    "        (HUNGRY, _('hungry')),\n",
    "        (SWEATY, _('sweaty')),\n",
    "        (SAVING_THE_WORLD, _('saving the world')),\n",
    "    ]\n",
    "    state = models.CharField(\n",
    "        _('state'),\n",
    "        max_length=100,\n",
    "        choices=STATE_TYPES,\n",
    "        default=ASLEEP,\n",
    "        help_text=_('actual state'),\n",
    "    )\n",
    "\n",
    "\n",
    "@receiver(post_init, sender=ModelWithState)\n",
    "def init_state_machine(instance, **kwargs):\n",
    "\n",
    "    states = [state for state, _ in instance.STATE_TYPES]\n",
    "    machine = instance.machine = Machine(model=instance, states=states, initial=instance.state)\n",
    "    machine.add_transition('work_out', instance.HANGING_OUT, instance.HUNGRY)\n",
    "    machine.add_transition('eat', instance.HUNGRY, instance.HANGING_OUT)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `transitions` memory footprint is too large for my Django app and adding models takes too long.\n",
    "\n",
    "We analyzed the memory footprint of `transitions` in [this discussion](https://github.com/pytransitions/transitions/issues/146) and could verify that the standard approach is not suitable to handle thousands of models. However, with a static (class) machine and some `__getattribute__` tweaking we can keep the convenience loss minimal:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from transitions import Machine\n",
    "from functools import partial\n",
    "from mock import MagicMock\n",
    "\n",
    "\n",
    "class Model(object):\n",
    "\n",
    "    machine = Machine(model=None, states=['A', 'B', 'C'], initial=None,\n",
    "                      transitions=[\n",
    "                          {'trigger': 'go', 'source': 'A', 'dest': 'B', 'before': 'before'},\n",
    "                          {'trigger': 'check', 'source': 'B', 'dest': 'C', 'conditions': 'is_large'},\n",
    "                      ], finalize_event='finalize')\n",
    "\n",
    "    def __init__(self):\n",
    "        self.state = 'A'\n",
    "        self.before = MagicMock()\n",
    "        self.after = MagicMock()\n",
    "        self.finalize = MagicMock()\n",
    "\n",
    "    @staticmethod\n",
    "    def is_large(value=0):\n",
    "        return value > 9000\n",
    "\n",
    "    def __getattribute__(self, item):\n",
    "        try:\n",
    "            return super(Model, self).__getattribute__(item)\n",
    "        except AttributeError:\n",
    "            if item in self.machine.events:\n",
    "                return partial(self.machine.events[item].trigger, self)\n",
    "            raise\n",
    "\n",
    "\n",
    "model = Model()\n",
    "model.go()\n",
    "assert model.state == 'B'\n",
    "assert model.before.called\n",
    "assert model.finalize.called\n",
    "model.check()\n",
    "assert model.state == 'B'\n",
    "model.check(value=500)\n",
    "assert model.state == 'B'\n",
    "model.check(value=9001)\n",
    "assert model.state == 'C'\n",
    "assert model.finalize.call_count == 4"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Is there a 'during' callback which is called when no transition has been successful?\n",
    "\n",
    "Currently, `transitions` has no such callback. This example from the issue discussed [here](https://github.com/pytransitions/transitions/issues/342) might give you a basic idea about how to extend `Machine` with such a feature:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from transitions.core import Machine, State, Event, EventData, listify\n",
    "\n",
    "\n",
    "class DuringState(State):\n",
    "\n",
    "    # add `on_during` to the dynamic callback methods\n",
    "    # this way on_during_<state> can be recognized by `Machine`\n",
    "    dynamic_methods = State.dynamic_methods + ['on_during']\n",
    "    \n",
    "    # parse 'during' and remove the keyword before passing the rest along to state\n",
    "    def __init__(self, *args, **kwargs):\n",
    "        during = kwargs.pop('during', [])\n",
    "        self.on_during = listify(during)\n",
    "        super(DuringState, self).__init__(*args, **kwargs)\n",
    "\n",
    "    def during(self, event_data):\n",
    "        for handle in self.on_during:\n",
    "            event_data.machine.callback(handle, event_data)\n",
    "\n",
    "\n",
    "class DuringEvent(Event):\n",
    "\n",
    "    def _trigger(self, model, *args, **kwargs):\n",
    "        # a successful transition returns `res=True` if res is False, we know that\n",
    "        # no transition has been executed\n",
    "        res = super(DuringEvent, self)._trigger(model, *args, **kwargs)\n",
    "        if res is False:\n",
    "            state = self.machine.get_state(model.state)\n",
    "            event_data = EventData(state, self, self.machine, model, args=args, kwargs=kwargs)\n",
    "            event_data.result = res\n",
    "            state.during(event_data)\n",
    "        return res\n",
    "\n",
    "\n",
    "class DuringMachine(Machine):\n",
    "    # we need to override the state and event classes used by `Machine`\n",
    "    state_cls = DuringState\n",
    "    event_cls = DuringEvent\n",
    "\n",
    "\n",
    "class Model:\n",
    "\n",
    "    def on_during_A(self):\n",
    "        print(\"Dynamically assigned callback\")\n",
    "\n",
    "    def another_callback(self):\n",
    "        print(\"Explicitly assigned callback\")\n",
    "\n",
    "\n",
    "model = Model()\n",
    "machine = DuringMachine(model=model, states=[{'name': 'A', 'during': 'another_callback'}, 'B'],\n",
    "                        transitions=[['go', 'B', 'A']], initial='A', ignore_invalid_triggers=True)\n",
    "machine.add_transition('test', source='A', dest='A', conditions=lambda: False)\n",
    "\n",
    "assert not model.go()\n",
    "assert not model.test()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### How to have a dynamic transition destination based on a function's return value\n",
    "\n",
    "This has been a feature request [here](https://github.com/pytransitions/transitions/issues/269). We'd encourage to write a wrapper which converts a condensed statement into individual condition-based transitions. However, a less expressive version could look like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from transitions import Machine, Transition\n",
    "from six import string_types\n",
    "\n",
    "class DependingTransition(Transition):\n",
    "\n",
    "    def __init__(self, source, dest, conditions=None, unless=None, before=None,\n",
    "                 after=None, prepare=None, **kwargs):\n",
    "\n",
    "        self._result = self._dest = None\n",
    "        super(DependingTransition, self).__init__(source, dest, conditions, unless, before, after, prepare)\n",
    "        if isinstance(dest, dict):\n",
    "            try:\n",
    "                self._func = kwargs.pop('depends_on')\n",
    "            except KeyError:\n",
    "                raise AttributeError(\"A multi-destination transition requires a 'depends_on'\")\n",
    "        else:\n",
    "            # use base version in case transition does not need special handling\n",
    "            self.execute = super(DependingTransition, self).execute\n",
    "\n",
    "    def execute(self, event_data):\n",
    "        func = getattr(event_data.model, self._func) if isinstance(self._func, string_types) \\\n",
    "               else self._func\n",
    "        self._result = func(*event_data.args, **event_data.kwargs)\n",
    "        super(DependingTransition, self).execute(event_data)\n",
    "\n",
    "    @property\n",
    "    def dest(self):\n",
    "        return self._dest[self._result] if self._result is not None else self._dest\n",
    "\n",
    "    @dest.setter\n",
    "    def dest(self, value):\n",
    "        self._dest = value\n",
    "\n",
    "# subclass Machine to use DependingTransition instead of standard Transition\n",
    "class DependingMachine(Machine):\n",
    "    transition_cls = DependingTransition\n",
    "    \n",
    "\n",
    "def func(value):\n",
    "    return value\n",
    "\n",
    "m = DependingMachine(states=['A', 'B', 'C', 'D'], initial='A')\n",
    "# define a dynamic transition with a 'depends_on' function which will return the required value\n",
    "m.add_transition(trigger='shuffle', source='A', dest=({1: 'B', 2: 'C', 3: 'D'}), depends_on=func)\n",
    "m.shuffle(value=2)  # func returns 2 which makes the transition dest to be 'C'\n",
    "assert m.is_C()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that this solution has some drawbacks. For instance, the generated graph might not include all possible outcomes."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `Machine.get_triggers` should only show valid transitions based on some conditions.\n",
    "\n",
    "This has been requested [here](https://github.com/pytransitions/transitions/issues/256). `Machine.get_triggers` is usually quite naive and only checks for theoretically possible transitions. If you need more sophisticated peeking, this `PeekMachine._can_trigger` might be a solution:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from transitions import Machine, EventData\n",
    "from functools import partial\n",
    "\n",
    "\n",
    "class Model(object):\n",
    "\n",
    "    def fails(self, condition=False):\n",
    "        return False\n",
    "\n",
    "    def success(self, condition=False):\n",
    "        return True\n",
    "\n",
    "    # condition is passed by EventData\n",
    "    def depends_on(self, condition=False):\n",
    "        return condition\n",
    "\n",
    "    def is_state_B(self, condition=False):\n",
    "        return self.state == 'B'\n",
    "\n",
    "\n",
    "class PeekMachine(Machine):\n",
    "\n",
    "    def _can_trigger(self, model, *args, **kwargs):\n",
    "        # We can omit the first two arguments state and event since they are only needed for \n",
    "        # actual state transitions. We do have to pass the machine (self) and the model as well as \n",
    "        # args and kwargs meant for the callbacks.\n",
    "        e = EventData(None, None, self, model, args, kwargs)\n",
    "\n",
    "        return [trigger_name for trigger_name in self.get_triggers(model.state)\n",
    "                if any(all(c.check(e) for c in t.conditions)\n",
    "                       for ts in self.events[trigger_name].transitions.values()\n",
    "                       for t in ts)]\n",
    "\n",
    "    # override Machine.add_model to assign 'can_trigger' to the model\n",
    "    def add_model(self, model, initial=None):\n",
    "        super(PeekMachine, self).add_model(model, initial)\n",
    "        setattr(model, 'can_trigger', partial(self._can_trigger, model))\n",
    "\n",
    "\n",
    "states = ['A', 'B', 'C', 'D']\n",
    "transitions = [\n",
    "    dict(trigger='go_A', source='*', dest='A', conditions=['depends_on']),  # only available when condition=True is passed\n",
    "    dict(trigger='go_B', source='*', dest='B', conditions=['success']),  # always available\n",
    "    dict(trigger='go_C', source='*', dest='C', conditions=['fails']),  # never available\n",
    "    dict(trigger='go_D', source='*', dest='D', conditions=['is_state_B']),  # only available in state B\n",
    "    dict(trigger='reset', source='D', dest='A', conditions=['success', 'depends_on']), # only available in state D when condition=True is passed\n",
    "    dict(trigger='forwards', source='A', dest='D', conditions=['success', 'fails']),  # never available\n",
    "]\n",
    "\n",
    "model = Model()\n",
    "machine = PeekMachine(model, states=states, transitions=transitions, initial='A', auto_transitions=False)\n",
    "assert model.can_trigger() == ['go_B']\n",
    "assert set(model.can_trigger(condition=True)) == set(['go_A', 'go_B'])\n",
    "model.go_B(condition=True)\n",
    "assert set(model.can_trigger()) == set(['go_B', 'go_D'])\n",
    "model.go_D()\n",
    "assert model.can_trigger() == ['go_B']\n",
    "assert set(model.can_trigger(condition=True)) == set(['go_A', 'go_B', 'reset'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Transitions does not add convencience methods to my model\n",
    "\n",
    "There is a high chance that your model *already contained* a `trigger` method or methods with the same name as your even trigger. In this case, `transitions` will not add convenience methods to not accidentaly break your model and only emit a warning. If you defined these methods on purpose and *want* them to be overrided or maybe even call *both* -- the trigger event AND your predefined method, you can extend/override `Machine._checked_assignment` which is always called when something needs to be added to a model:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from transitions import State, Machine\n",
    "\n",
    "class StateMachineModel:\n",
    "\n",
    "    state = None\n",
    "\n",
    "    def __init__(self):\n",
    "        pass\n",
    "\n",
    "    def transition_one(self):\n",
    "        print('transitioning states...')\n",
    "\n",
    "    def transition_two(self):\n",
    "        print('transitioning states...')\n",
    "\n",
    "\n",
    "class OverrideMachine(Machine):\n",
    "\n",
    "    def _checked_assignment(self, model, name, func):\n",
    "        setattr(model, name, func)\n",
    "\n",
    "\n",
    "class CallingMachine(Machine):\n",
    "\n",
    "    def _checked_assignment(self, model, name, func):\n",
    "        if hasattr(model, name):\n",
    "            predefined_func = getattr(model, name)\n",
    "            def nested_func(*args, **kwargs):\n",
    "                predefined_func()\n",
    "                func(*args, **kwargs)\n",
    "            setattr(model, name, nested_func)\n",
    "        else:\n",
    "            setattr(model, name, func)\n",
    "\n",
    "\n",
    "states = [State(name='A'), State(name='B'), State(name='C'), State(name='D')]\n",
    "transitions = [\n",
    "    {'trigger': 'transition_one', 'source': 'A', 'dest': 'B'},\n",
    "    {'trigger': 'transition_two', 'source': 'B', 'dest': 'C'},\n",
    "    {'trigger': 'transition_three', 'source': 'C', 'dest': 'D'}\n",
    "]\n",
    "state_machine_model = StateMachineModel()\n",
    "\n",
    "print('OverrideMachine ...')\n",
    "state_machine = OverrideMachine(model=state_machine_model, states=states, transitions=transitions, initial=states[0])\n",
    "print('state_machine_model (current state): %s' % state_machine_model.state)\n",
    "state_machine_model.transition_one()\n",
    "print('state_machine_model (current state): %s' % state_machine_model.state)\n",
    "state_machine_model.transition_two()\n",
    "print('state_machine_model (current state): %s' % state_machine_model.state)\n",
    "\n",
    "print('\\nCallingMachine ...')\n",
    "state_machine_model = StateMachineModel()\n",
    "state_machine = CallingMachine(model=state_machine_model, states=states, transitions=transitions, initial=states[0])\n",
    "print('state_machine_model (current state): %s' % state_machine_model.state)\n",
    "state_machine_model.transition_one()\n",
    "print('state_machine_model (current state): %s' % state_machine_model.state)\n",
    "state_machine_model.transition_two()\n",
    "print('state_machine_model (current state): %s' % state_machine_model.state)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.8.3 64-bit ('transitions': conda)",
   "language": "python",
   "name": "python38364bittransitionsconda9f9fdeb4313741768b0dccf7fd8ce480"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
